package com.atzly.community.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.atzly.community.entity.DiscussPost;
import com.atzly.community.mapper.DiscussPostMapper;
import com.atzly.community.service.DiscussPostService;
import com.atzly.community.utils.CommunityUtils;
import com.atzly.community.utils.RedisKeyUtils;
import com.atzly.community.utils.SensitiveFilter;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.web.util.HtmlUtils;

import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 性能优化: 使用二级缓存
 * caffeine->redis->mysql
 *
 * @author zlysimida
 * @date 2021/8/25 - 11:42
 */
@Service
public class DiscussPostServiceImpl implements DiscussPostService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DiscussPostServiceImpl.class);

    @Autowired
    private DiscussPostMapper discussPostMapper;

    @Autowired
    private SensitiveFilter sensitiveFilter;

    @Value("${caffeine.posts.max-size}")
    private int maxSize;

    @Value("${caffeine.posts.expire-seconds}")
    private int expireTime;

    // caffeine缓存实现形式  LoadingCache(阻塞), AsyncLoadingCache(多个线程同时操作该缓存)
    private LoadingCache<String, List<DiscussPost>> postCache;

    private LoadingCache<Integer, Integer> numsCache;

    // 二级缓存
    @Autowired
    private RedisTemplate redisTemplate;

    @PostConstruct
    public void init() {
        // 初始化帖子列表缓存
        postCache = Caffeine.newBuilder()
                .maximumSize(maxSize)
                .expireAfterWrite(expireTime, TimeUnit.SECONDS)
                .build(new CacheLoader<String, List<DiscussPost>>() {
                    // 按照该方法处理缓存中的数据
                    @Override
                    public @Nullable List<DiscussPost> load(@NonNull String key) throws Exception {
                        if (StringUtils.isBlank(key)) {
                            throw new IllegalArgumentException("caffeine参数错误!");
                        }

                        String[] params = key.split(":");
                        if (params == null || params.length != 2) {
                            throw new IllegalArgumentException("caffeine参数错误!");
                        }

                        int offset = Integer.parseInt(params[0]);
                        int limit = Integer.parseInt(params[1]);

                        // TODO 这里添加二级缓存(注意高并发下缓存失效问题)
                        // redis中的key hot:offset:limit
                        String hotDateKey = RedisKeyUtils.getHotDateKey(offset, limit);
                        String jsonPosts = (String) redisTemplate.opsForValue().get(hotDateKey);
                        List<DiscussPost> posts = (List<DiscussPost>) JSONObject.parse(jsonPosts);
                        if (posts == null || posts.size() < 1) {
                            // redis中不存在
                            return addPostsToRedis(hotDateKey, offset, limit);
                        }
                        LOGGER.info("loader cache posts date start");
                        return discussPostMapper.selectDiscussPost(0,offset,limit,1);
                    }
                });
        // 初始化缓存数据
        numsCache = Caffeine.newBuilder()
                .maximumSize(maxSize)
                .expireAfterWrite(expireTime, TimeUnit.SECONDS)
                .build(new CacheLoader<Integer, Integer>() {
                    @Override
                    public @Nullable Integer load(@NonNull Integer key) throws Exception {
                        if (key == null || key != 0) {
                            throw new IllegalArgumentException("caffeine参数错误!");
                        }

                        // TODO 添加二级缓存
//                        String hotNumKey = RedisKeyUtils.getHotNumKey(key);
//                        Integer num = (Integer) redisTemplate.opsForValue().get(hotNumKey);
//                        if(num == null) {
//
//                        }
                        LOGGER.info("loader cache counts date start");
                        return discussPostMapper.selectDiscussByUserId(key);
                    }
                });
    }

    private final Object lock = new Object();
    private static volatile int counts = 0;

    // 添加热帖到redis中
    private List<DiscussPost> addPostsToRedis(String redisKey, int offset, int limit) {
        List<DiscussPost> posts = (List<DiscussPost>) redisTemplate.opsForValue().get(redisKey);
        while (counts == 0) {
            synchronized (lock) {   // 加锁防止缓存穿透问题每次只能一个线程去访问数据库查询
                if (counts == 0) {
                    posts = discussPostMapper.selectDiscussPost(0, offset, limit, 1);
                    // 设置随机过期时间 防止缓存雪崩
                    String json = null;
                    if (posts != null) {
                        json = JSONObject.toJSONString(posts);
                    }
                    long expire = expireTime + (long) (1 + (Math.random() * 100));
                    redisTemplate.opsForValue().set(redisKey, json, expire, TimeUnit.SECONDS);
                    // 如果此时数据库中查询的数据为空，那么也通知其他线程cache中有值，防止缓存穿透问题
                    counts++;
                }
            }
        }
        return posts;
    }

    private List<DiscussPost> addPostsToRedisByNX(String redisKey, int offset, int limit) {
        // 加锁原子性操作
        // set ex nx
        String lockValue = CommunityUtils.randomString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", lockValue, 300, TimeUnit.SECONDS);
        if (lock) {
            // 业务代码
            List<DiscussPost> posts;
            try {
                posts = (List<DiscussPost>) redisTemplate.opsForValue().get(redisKey);
                if (posts == null && posts.size() < 1) {
                    posts = discussPostMapper.selectDiscussPost(0, offset, limit, 1);
                    redisTemplate.opsForValue().set(redisKey, posts, expireTime, TimeUnit.SECONDS);
                }
            } finally {
                // 解锁使用Lua脚本
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList("lock"), lockValue);
            }
            return posts;
        } else {
            // 重试的获取锁
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return addPostsToRedis(redisKey, offset, limit);
        }
    }


    @Override
    public List<DiscussPost> selectDiscussPost(Integer userId, Integer offset, Integer limit, int orderMode) {
        // 热帖缓存(只缓存第一页帖子)
        if (userId == 0 && orderMode == 1) {
            return postCache.get(offset + ":" + limit);
        }
        return discussPostMapper.selectDiscussPost(userId, offset, limit, orderMode);
    }

    @Override
    public Integer selectDiscussByUserId(Integer userId) {
        // 热帖数量
        if (userId == 0 || userId == null) {
            // 默认score>2的为热帖
            return numsCache.get(0);
        }
        return discussPostMapper.selectDiscussByUserId(userId);
    }

    @Override
    public void insertDiscussPost(DiscussPost discussPost) {
        discussPostMapper.insertDiscussPost(discussPost);
    }

    @Override
    public void insertDiscussPostAndPrase(DiscussPost post) {
        if (post == null) {
            throw new IllegalArgumentException("参数不能为空!");
        }
        String title = HtmlUtils.htmlEscape(post.getTitle());
        String content = HtmlUtils.htmlEscape(post.getContent());
        // 过滤敏感词
        post.setTitle(sensitiveFilter.filter(title));
        post.setContent(sensitiveFilter.filter(content));
        discussPostMapper.insertDiscussPost(post);
    }

    @Override
    public DiscussPost selectDiscussDetails(int id) {
        return discussPostMapper.selectDiscussDetails(id);
    }

    @Override
    public int updateDiscussPostCounts(int id, int commentCount) {
        return discussPostMapper.updateDiscussPostCounts(id, commentCount);
    }

    @Override
    public int updateDiscussPostStatus(int id, int status) {
        return discussPostMapper.updateDiscussPostStatus(id, status);
    }

    @Override
    public int updateDiscussPostType(int id, int type) {
        return discussPostMapper.updateDiscussPostType(id, type);
    }

    @Override
    public void updateDiscussPostScore(int postId, double score) {
        discussPostMapper.updateDiscussPostScore(postId, score);
    }

    @Override
    public int getHotPostNums(int score) {
        return discussPostMapper.getHotPostNums(score);
    }
}

